/*****************************************************************************
   libppf v0.1-rc1 - A library for handling PPF patch files
  
   Copyright (C), Daniel Ekstr�m <dv01dem@cs.umu.se>, 2007 - 2008
   More information can be found at http://oakstream.mine.nu

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
******************************************************************************/

// Unless specified, every function returns 0 for success or an error code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "libppf.h"

#define DEFAULT_CHUNK_ALLOC 4096

static void addChunk(ppf_t *ppf, ppf_chunk_t *chunk) {
   // reallocate if out of room
   if (ppf->chunkCount >= ppf->chunkAlloc) {
      ppf_chunk_t *newChunks;
      int newAlloc = ppf->chunkAlloc * 2;
      newChunks = malloc(newAlloc * sizeof(ppf_chunk_t));
      memcpy(newChunks, ppf->chunks, ppf->chunkAlloc * sizeof(ppf_chunk_t));
      free(ppf->chunks);
      ppf->chunks = newChunks;
      ppf->chunkAlloc = newAlloc;
   }
   memcpy(&(ppf->chunks[ppf->chunkCount]), chunk, sizeof(ppf_chunk_t));
   ppf->chunkCount++;
}

/*
 * Takes a user defined size char array of data creates a long integer
 * representing the data offset in big endian order
 */
static void chunkSetOffset(ppf_chunk_t *chunk, unsigned char *buf, int bytes) {
   int i;
   bool start = false;

   // Reset current data offset value
   chunk->offset = 0;

   // Go through array to calculate
   for (i = bytes - 1; i > -1; i--) {
      // Make sure that the leading zeroes won't be used
      if (start == false && buf[i] != 0)
         start = true;
      if (start)
         chunk->offset += (long)buf[i] * (1L << (i * 8));
   }
}

void ppf_init(ppf_t *ppf)
{
   // Set default options
   ppf->loaded = false;
   ppf->ppfDesc = "";
   ppf->ppfName = "";
   ppf->isoName = "";
   ppf->totalSize = 0;
   ppf->type = TYPE_BIN;
   ppf->hasUndo = false;
   ppf->validation = false;
   ppf->version = 1;
   ppf->chunks = malloc(DEFAULT_CHUNK_ALLOC * sizeof(ppf_chunk_t));
   ppf->chunkAlloc = DEFAULT_CHUNK_ALLOC;
   ppf->chunkCount = 0;
}

/*
 * Load and parse the given PPF patch file
 */
int ppf_loadPatch(ppf_t *ppf, char *filename) {
   FILE *file;
   unsigned char buf[256];
   int chunkSize, offsetSize, size;
   bool repeat;

   // Save filename
   ppf->ppfName = filename;

   // Try to open the patch
   if ((file = fopen(filename, "rb")) == NULL)
      return ERROR_PPF_OPEN;

   // Check if this really is a PPF file
   if (fread(buf, 1, 5, file) != 5)
      return ERROR_PPF_READ;

   if (buf[0] != 'P' || buf[1] != 'P' || buf[2] != 'F')
      return ERROR_PPF_FORMAT;

   // Get version number
   ppf->version = fgetc(file) + 1;

   // Get patch description
   if (fread(buf, 1, 50, file) != 50)
      return ERROR_PPF_READ;

   // Add it as a string
   ppf->ppfDesc = malloc(64);
   memcpy(ppf->ppfDesc, buf, 50);
   // TODO: null terminate?

   // The following only applies to PPF v3.0
   if (ppf->version == 3) {
      // Get image type
      ppf->type = fgetc(file);

      // Find out if validation is available
      if (fgetc(file) == 0x01)
         ppf->validation = true;

      // Find out if undo data is available
      if (fgetc(file) == 0x01)
         ppf->hasUndo = true;
   }

   // Set patch file pointer at the data position and set offset size,
   // which is 32-bit (4 bytes) for PPFv1 and 64-bit (8 bytes) for PPFv3
   if (ppf->version == 1) {
      offsetSize = 4;
      fseek(file, 56, SEEK_SET);
   } else if (ppf->version == 3) {
      offsetSize = 8;
      if (ppf->validation)
         fseek(file, 1084, SEEK_SET);
      else
         fseek(file, 60, SEEK_SET);
   } else {
      return ERROR_PPF_VERSION;
   }

   while ((size = fread(buf, 1, offsetSize, file)) != 0) {
      // Size must be 0, or offsetSize
      if (size != offsetSize)
         return ERROR_PPF_READ;

      // Create a new data chunk
      ppf_chunk_t chunk;
      chunk.offset = 0;
      chunk.size = 0;

      // Get offset
      chunkSetOffset(&chunk, buf, offsetSize);

      // Read chunk size and add it to the total chunk size
      if (fread(buf, 1, 1, file) != 1)
         return ERROR_PPF_READ;
      else
         chunkSize = buf[0];
      ppf->totalSize += chunkSize;

      // Read and add chunk data. If chunkSize is 0, the first byte is the
      // data and the second is number of repetitions. This is only valid
      // for PPFv1 patches
      repeat = false;
      if (chunkSize == 0) {
         if (ppf->version == 3)
            return ERROR_PPF_FORMAT;
         else if (ppf->version == 1) {
            chunkSize = 2;
            repeat = true;
         }
      }

      if ((signed)fread(buf, 1, chunkSize, file) != chunkSize)
         return ERROR_PPF_READ;
      else {
         chunk.size = chunkSize;
         if (repeat) {
            // If size is 2, the first byte in buffer is the data and the second
            // is number of repetitions of this data
            memset(chunk.data, buf[0], buf[1]);
         } else {
            memcpy(chunk.data, buf, chunkSize);
         }
      }

      // Read and add optional undo data
      if (ppf->version == 3 && ppf->hasUndo) {
         if ((signed)fread(buf, 1, chunkSize, file) != chunkSize)
            return ERROR_PPF_READ;
         else {
            memcpy(chunk.undo, buf, chunkSize);
         }
      }

      // Add chunk to ppf vector
      addChunk(ppf, &chunk);
   }

   // Close file
   if (fclose(file) != 0)
      return ERROR_PPF_CLOSE;

   // Set patch as loaded
   ppf->loaded = true;

   return 0;
}

/*
 * Apply the loaded patch or undo data on the given file
 */
int ppf_applyPatch(ppf_t *ppf, char *filename, bool undo) {
   FILE *file;
   unsigned i;

   // Save filename
   ppf->isoName = filename;

   // Make sure that the patch has been loaded and that undo data is
   // available if specified
   if (!ppf->loaded)
      return ERROR_PPF_LOADED;
   if (undo && !ppf->hasUndo)
      return ERROR_PPF_UNDO;

   // Make sure that file exists by trying to open it in read mode first
   if ((file = fopen(filename, "rb")) == NULL)
      return ERROR_ISO_EXISTS;
   else
      fclose(file);

   // Open the given file
   if ((file = fopen(filename, "r+b")) == NULL)
      return ERROR_ISO_OPEN;

   // Go through patch chunks one by one
   for (i = 0; i < ppf->chunkCount; i++) {

      // Position binary file pointer at this chunk's offset
      fseek(file, ppf->chunks[i].offset, SEEK_SET);

      // Write chunk data or undo data to file
      unsigned chunkSize = ppf->chunks[i].size;
      if (undo) {
         if (fwrite(ppf->chunks[i].undo, 1, chunkSize, file) != chunkSize)
            return ERROR_ISO_WRITE;
      } else {
         if (fwrite(ppf->chunks[i].data, 1, chunkSize, file) != chunkSize)
            return ERROR_ISO_WRITE;
      }
   }

   // Close files
   if (fclose(file) == -1)
      return ERROR_ISO_CLOSE;

   return 0;
}

/*
 * Print patch info / status
 */
void ppf_dumpInfo(ppf_t *ppf) {
   fprintf(stderr, "Linux PPF Patcher v0.1-rc1\n\n");
   fprintf(stderr, "Patch info:\n");
   fprintf(stderr, "   Name:       %s\n", ppf->ppfName);
   fprintf(stderr, "   Version:    %d.0\n", ppf->version);
   fprintf(stderr, "   Chunks:     %d\n", ppf->chunkCount);
   fprintf(stderr, "   Total Size: %d\n", ppf->totalSize);

   fprintf(stderr, "   Image type: ");
   if (ppf->type == TYPE_BIN)
      fprintf(stderr, "BIN\n");
   else if (ppf->type == TYPE_GI)
      fprintf(stderr, "GI\n");
   else
      fprintf(stderr, "Unknown\n");

   fprintf(stderr, "   Validation: ");
   if (ppf->validation)
      fprintf(stderr, "Available\n");
   else
      fprintf(stderr, "Not available\n");

   fprintf(stderr, "   Undo data:  ");
   if (ppf->hasUndo)
      fprintf(stderr, "Available\n");
   else
      fprintf(stderr, "Not available\n");

   // fprintf(stderr, "   Description: " << desc << "\n";
}
